using System;
using System.IO;
using System.Text;
using System.Reflection;
using System.ComponentModel;
using System.Text.RegularExpressions;
using System.Collections.Generic;
#if SILVERLIGHT
using System.Linq;
#endif

namespace PickGold.Zip
{

	/// <summary>
	/// Enumerates the options for a logical conjunction. This enum is intended for use
	/// internally by the FileSelector class.
	/// </summary>
	internal enum LogicalConjunction
	{
		NONE,
		AND,
		OR,
		XOR,
	}

	internal enum WhichTime
	{
		atime,
		mtime,
		ctime,
	}


	internal enum ComparisonOperator
	{
		[Description(">")]
		GreaterThan,
		[Description(">=")]
		GreaterThanOrEqualTo,
		[Description("<")]
		LesserThan,
		[Description("<=")]
		LesserThanOrEqualTo,
		[Description("=")]
		EqualTo,
		[Description("!=")]
		NotEqualTo
	}


	internal abstract partial class SelectionCriterion
	{
		internal virtual bool Verbose
		{
			get;
			set;
		}
		internal abstract bool Evaluate(string filename);

		[System.Diagnostics.Conditional("SelectorTrace")]
		protected static void CriterionTrace(string format, params object[] args)
		{
			//System.Console.WriteLine("  " + format, args);
		}
	}


	internal partial class SizeCriterion : SelectionCriterion
	{
		internal ComparisonOperator Operator;
		internal Int64 Size;

		public override String ToString()
		{
			StringBuilder sb = new StringBuilder();
			sb.Append("size ").Append(EnumUtil.GetDescription(Operator)).Append(" ").Append(Size.ToString());
			return sb.ToString();
		}

		internal override bool Evaluate(string filename)
		{
			System.IO.FileInfo fi = new System.IO.FileInfo(filename);
			CriterionTrace("SizeCriterion::Evaluate('{0}' [{1}])",
						   filename, this.ToString());
			return _Evaluate(fi.Length);
		}

		private bool _Evaluate(Int64 Length)
		{
			bool result = false;
			switch (Operator)
			{
				case ComparisonOperator.GreaterThanOrEqualTo:
					result = Length >= Size;
					break;
				case ComparisonOperator.GreaterThan:
					result = Length > Size;
					break;
				case ComparisonOperator.LesserThanOrEqualTo:
					result = Length <= Size;
					break;
				case ComparisonOperator.LesserThan:
					result = Length < Size;
					break;
				case ComparisonOperator.EqualTo:
					result = Length == Size;
					break;
				case ComparisonOperator.NotEqualTo:
					result = Length != Size;
					break;
				default:
					throw new ArgumentException("Operator");
			}
			return result;
		}

	}



	internal partial class TimeCriterion : SelectionCriterion
	{
		internal ComparisonOperator Operator;
		internal WhichTime Which;
		internal DateTime Time;

		public override String ToString()
		{
			StringBuilder sb = new StringBuilder();
			sb.Append(Which.ToString()).Append(" ").Append(EnumUtil.GetDescription(Operator)).Append(" ").Append(Time.ToString("yyyy-MM-dd-HH:mm:ss"));
			return sb.ToString();
		}

		internal override bool Evaluate(string filename)
		{
			DateTime x;
			switch (Which)
			{
				case WhichTime.atime:
					x = System.IO.File.GetLastAccessTime(filename).ToUniversalTime();
					break;
				case WhichTime.mtime:
					x = System.IO.File.GetLastWriteTime(filename).ToUniversalTime();
					break;
				case WhichTime.ctime:
					x = System.IO.File.GetCreationTime(filename).ToUniversalTime();
					break;
				default:
					throw new ArgumentException("Operator");
			}
			CriterionTrace("TimeCriterion({0},{1})= {2}", filename, Which.ToString(), x);
			return _Evaluate(x);
		}


		private bool _Evaluate(DateTime x)
		{
			bool result = false;
			switch (Operator)
			{
				case ComparisonOperator.GreaterThanOrEqualTo:
					result = (x >= Time);
					break;
				case ComparisonOperator.GreaterThan:
					result = (x > Time);
					break;
				case ComparisonOperator.LesserThanOrEqualTo:
					result = (x <= Time);
					break;
				case ComparisonOperator.LesserThan:
					result = (x < Time);
					break;
				case ComparisonOperator.EqualTo:
					result = (x == Time);
					break;
				case ComparisonOperator.NotEqualTo:
					result = (x != Time);
					break;
				default:
					throw new ArgumentException("Operator");
			}

			CriterionTrace("TimeCriterion: {0}", result);
			return result;
		}
	}



	internal partial class NameCriterion : SelectionCriterion
	{
		private Regex _re;
		private String _regexString;
		internal ComparisonOperator Operator;
		private string _MatchingFileSpec;
		internal virtual string MatchingFileSpec
		{
			set
			{
				// workitem 8245
				if (Directory.Exists(value))
				{
					_MatchingFileSpec = ".\\" + value + "\\*.*";
				}
				else
				{
					_MatchingFileSpec = value;
				}

				_regexString = "^" +
				Regex.Escape(_MatchingFileSpec)
					.Replace(@"\\\*\.\*", @"\\([^\.]+|.*\.[^\\\.]*)")
					.Replace(@"\.\*", @"\.[^\\\.]*")
					.Replace(@"\*", @".*")
					//.Replace(@"\*", @"[^\\\.]*") // ill-conceived
					.Replace(@"\?", @"[^\\\.]")
					+ "$";

				CriterionTrace("NameCriterion regexString({0})", _regexString);

				_re = new Regex(_regexString, RegexOptions.IgnoreCase);
			}
		}


		public override String ToString()
		{
			StringBuilder sb = new StringBuilder();
			sb.Append("name ").Append(EnumUtil.GetDescription(Operator))
				.Append(" '")
				.Append(_MatchingFileSpec)
				.Append("'");
			return sb.ToString();
		}


		internal override bool Evaluate(string filename)
		{
			CriterionTrace("NameCriterion::Evaluate('{0}' pattern[{1}])",
						   filename, _MatchingFileSpec);
			return _Evaluate(filename);
		}

		private bool _Evaluate(string fullpath)
		{
			CriterionTrace("NameCriterion::Evaluate({0})", fullpath);
			// No slash in the pattern implicitly means recurse, which means compare to
			// filename only, not full path.
			String f = (_MatchingFileSpec.IndexOf('\\') == -1)
				? System.IO.Path.GetFileName(fullpath)
				: fullpath; // compare to fullpath

			bool result = _re.IsMatch(f);

			if (Operator != ComparisonOperator.EqualTo)
				result = !result;
			return result;
		}
	}


	internal partial class TypeCriterion : SelectionCriterion
	{
		private char ObjectType;  // 'D' = Directory, 'F' = File
		internal ComparisonOperator Operator;
		internal string AttributeString
		{
			get
			{
				return ObjectType.ToString();
			}
			set
			{
				if (value.Length != 1 ||
					(value[0] != 'D' && value[0] != 'F'))
					throw new ArgumentException("Specify a single character: either D or F");
				ObjectType = value[0];
			}
		}

		public override String ToString()
		{
			StringBuilder sb = new StringBuilder();
			sb.Append("type ").Append(EnumUtil.GetDescription(Operator)).Append(" ").Append(AttributeString);
			return sb.ToString();
		}

		internal override bool Evaluate(string filename)
		{
			CriterionTrace("TypeCriterion::Evaluate({0})", filename);

			bool result = (ObjectType == 'D')
				? Directory.Exists(filename)
				: File.Exists(filename);

			if (Operator != ComparisonOperator.EqualTo)
				result = !result;
			return result;
		}
	}


#if !SILVERLIGHT
	internal partial class AttributesCriterion : SelectionCriterion
	{
		private FileAttributes _Attributes;
		internal ComparisonOperator Operator;
		internal string AttributeString
		{
			get
			{
				string result = "";
				if ((_Attributes & FileAttributes.Hidden) != 0)
					result += "H";
				if ((_Attributes & FileAttributes.System) != 0)
					result += "S";
				if ((_Attributes & FileAttributes.ReadOnly) != 0)
					result += "R";
				if ((_Attributes & FileAttributes.Archive) != 0)
					result += "A";
				if ((_Attributes & FileAttributes.ReparsePoint) != 0)
					result += "L";
				if ((_Attributes & FileAttributes.NotContentIndexed) != 0)
					result += "I";
				return result;
			}

			set
			{
				_Attributes = FileAttributes.Normal;
				foreach (char c in value.ToUpper())
				{
					switch (c)
					{
						case 'H':
							if ((_Attributes & FileAttributes.Hidden) != 0)
								throw new ArgumentException(String.Format("Repeated flag. ({0})", c), "value");
							_Attributes |= FileAttributes.Hidden;
							break;

						case 'R':
							if ((_Attributes & FileAttributes.ReadOnly) != 0)
								throw new ArgumentException(String.Format("Repeated flag. ({0})", c), "value");
							_Attributes |= FileAttributes.ReadOnly;
							break;

						case 'S':
							if ((_Attributes & FileAttributes.System) != 0)
								throw new ArgumentException(String.Format("Repeated flag. ({0})", c), "value");
							_Attributes |= FileAttributes.System;
							break;

						case 'A':
							if ((_Attributes & FileAttributes.Archive) != 0)
								throw new ArgumentException(String.Format("Repeated flag. ({0})", c), "value");
							_Attributes |= FileAttributes.Archive;
							break;

						case 'I':
							if ((_Attributes & FileAttributes.NotContentIndexed) != 0)
								throw new ArgumentException(String.Format("Repeated flag. ({0})", c), "value");
							_Attributes |= FileAttributes.NotContentIndexed;
							break;

						case 'L':
							if ((_Attributes & FileAttributes.ReparsePoint) != 0)
								throw new ArgumentException(String.Format("Repeated flag. ({0})", c), "value");
							_Attributes |= FileAttributes.ReparsePoint;
							break;

						default:
							throw new ArgumentException(value);
					}
				}
			}
		}


		public override String ToString()
		{
			StringBuilder sb = new StringBuilder();
			sb.Append("attributes ").Append(EnumUtil.GetDescription(Operator)).Append(" ").Append(AttributeString);
			return sb.ToString();
		}

		private bool _EvaluateOne(FileAttributes fileAttrs, FileAttributes criterionAttrs)
		{
			bool result = false;
			if ((_Attributes & criterionAttrs) == criterionAttrs)
				result = ((fileAttrs & criterionAttrs) == criterionAttrs);
			else
				result = true;
			return result;
		}



		internal override bool Evaluate(string filename)
		{
			// workitem 10191
			if (Directory.Exists(filename))
			{
				// Directories don't have file attributes, so the result
				// of an evaluation is always NO. This gets negated if
				// the operator is NotEqualTo.
				return (Operator != ComparisonOperator.EqualTo);
			}
#if NETCF
            FileAttributes fileAttrs = NetCfFile.GetAttributes(filename);
#else
			FileAttributes fileAttrs = System.IO.File.GetAttributes(filename);
#endif

			return _Evaluate(fileAttrs);
		}

		private bool _Evaluate(FileAttributes fileAttrs)
		{
			bool result = _EvaluateOne(fileAttrs, FileAttributes.Hidden);
			if (result)
				result = _EvaluateOne(fileAttrs, FileAttributes.System);
			if (result)
				result = _EvaluateOne(fileAttrs, FileAttributes.ReadOnly);
			if (result)
				result = _EvaluateOne(fileAttrs, FileAttributes.Archive);
			if (result)
				result = _EvaluateOne(fileAttrs, FileAttributes.NotContentIndexed);
			if (result)
				result = _EvaluateOne(fileAttrs, FileAttributes.ReparsePoint);

			if (Operator != ComparisonOperator.EqualTo)
				result = !result;

			return result;
		}
	}
#endif


	internal partial class CompoundCriterion : SelectionCriterion
	{
		internal LogicalConjunction Conjunction;
		internal SelectionCriterion Left;

		private SelectionCriterion _Right;
		internal SelectionCriterion Right
		{
			get { return _Right; }
			set
			{
				_Right = value;
				if (value == null)
					Conjunction = LogicalConjunction.NONE;
				else if (Conjunction == LogicalConjunction.NONE)
					Conjunction = LogicalConjunction.AND;
			}
		}


		internal override bool Evaluate(string filename)
		{
			bool result = Left.Evaluate(filename);
			switch (Conjunction)
			{
				case LogicalConjunction.AND:
					if (result)
						result = Right.Evaluate(filename);
					break;
				case LogicalConjunction.OR:
					if (!result)
						result = Right.Evaluate(filename);
					break;
				case LogicalConjunction.XOR:
					result ^= Right.Evaluate(filename);
					break;
				default:
					throw new ArgumentException("Conjunction");
			}
			return result;
		}


		public override String ToString()
		{
			StringBuilder sb = new StringBuilder();
			sb.Append("(")
			.Append((Left != null) ? Left.ToString() : "null")
			.Append(" ")
			.Append(Conjunction.ToString())
			.Append(" ")
			.Append((Right != null) ? Right.ToString() : "null")
			.Append(")");
			return sb.ToString();
		}
	}



	/// <summary>
	///   FileSelector encapsulates logic that selects files from a source - a zip file
	///   or the filesystem - based on a set of criteria.  This class is used internally
	///   by the DotNetZip library, in particular for the AddSelectedFiles() methods.
	///   This class can also be used independently of the zip capability in DotNetZip.
	/// </summary>
	///
	/// <remarks>
	///
	/// <para>
	///   The FileSelector class is used internally by the ZipFile class for selecting
	///   files for inclusion into the ZipFile, when the <see
	///   cref="PickGold.Zip.ZipFile.AddSelectedFiles(String,String)"/> method, or one of
	///   its overloads, is called.  It's also used for the <see
	///   cref="PickGold.Zip.ZipFile.ExtractSelectedEntries(String)"/> methods.  Typically, an
	///   application that creates or manipulates Zip archives will not directly
	///   interact with the FileSelector class.
	/// </para>
	///
	/// <para>
	///   Some applications may wish to use the FileSelector class directly, to
	///   select files from disk volumes based on a set of criteria, without creating or
	///   querying Zip archives.  The file selection criteria include: a pattern to
	///   match the filename; the last modified, created, or last accessed time of the
	///   file; the size of the file; and the attributes of the file.
	/// </para>
	///
	/// <para>
	///   Consult the documentation for <see cref="SelectionCriteria"/>
	///   for more information on specifying the selection criteria.
	/// </para>
	///
	/// </remarks>
	public partial class FileSelector
	{
		internal SelectionCriterion _Criterion;

#if NOTUSED
        /// <summary>
        ///   The default constructor.
        /// </summary>
        /// <remarks>
        ///   Typically, applications won't use this constructor.  Instead they'll
        ///   call the constructor that accepts a selectionCriteria string.  If you
        ///   use this constructor, you'll want to set the SelectionCriteria
        ///   property on the instance before calling SelectFiles().
        /// </remarks>
        protected FileSelector() { }
#endif
		/// <summary>
		///   Constructor that allows the caller to specify file selection criteria.
		/// </summary>
		///
		/// <remarks>
		/// <para>
		///   This constructor allows the caller to specify a set of criteria for
		///   selection of files.
		/// </para>
		///
		/// <para>
		///   See <see cref="FileSelector.SelectionCriteria"/> for a description of
		///   the syntax of the selectionCriteria string.
		/// </para>
		///
		/// <para>
		///   By default the FileSelector will traverse NTFS Reparse Points.  To
		///   change this, use <see cref="FileSelector(String,
		///   bool)">FileSelector(String, bool)</see>.
		/// </para>
		/// </remarks>
		///
		/// <param name="selectionCriteria">The criteria for file selection.</param>
		public FileSelector(String selectionCriteria)
			: this(selectionCriteria, true)
		{
		}

		/// <summary>
		///   Constructor that allows the caller to specify file selection criteria.
		/// </summary>
		///
		/// <remarks>
		/// <para>
		///   This constructor allows the caller to specify a set of criteria for
		///   selection of files.
		/// </para>
		///
		/// <para>
		///   See <see cref="FileSelector.SelectionCriteria"/> for a description of
		///   the syntax of the selectionCriteria string.
		/// </para>
		/// </remarks>
		///
		/// <param name="selectionCriteria">The criteria for file selection.</param>
		/// <param name="traverseDirectoryReparsePoints">
		/// whether to traverse NTFS reparse points (junctions).
		/// </param>
		public FileSelector(String selectionCriteria, bool traverseDirectoryReparsePoints)
		{
			if (!String.IsNullOrEmpty(selectionCriteria))
				_Criterion = _ParseCriterion(selectionCriteria);
			TraverseReparsePoints = traverseDirectoryReparsePoints;
		}



		/// <summary>
		///   The string specifying which files to include when retrieving.
		/// </summary>
		/// <remarks>
		///
		/// <para>
		///   Specify the criteria in statements of 3 elements: a noun, an operator,
		///   and a value.  Consider the string "name != *.doc" .  The noun is
		///   "name".  The operator is "!=", implying "Not Equal".  The value is
		///   "*.doc".  That criterion, in English, says "all files with a name that
		///   does not end in the .doc extension."
		/// </para>
		///
		/// <para>
		///   Supported nouns include "name" (or "filename") for the filename;
		///   "atime", "mtime", and "ctime" for last access time, last modfied time,
		///   and created time of the file, respectively; "attributes" (or "attrs")
		///   for the file attributes; "size" (or "length") for the file length
		///   (uncompressed); and "type" for the type of object, either a file or a
		///   directory.  The "attributes", "type", and "name" nouns all support =
		///   and != as operators.  The "size", "atime", "mtime", and "ctime" nouns
		///   support = and !=, and &gt;, &gt;=, &lt;, &lt;= as well.  The times are
		///   taken to be expressed in local time.
		/// </para>
		///
		/// <para>
		///   Specify values for the file attributes as a string with one or more of
		///   the characters H,R,S,A,I,L in any order, implying file attributes of
		///   Hidden, ReadOnly, System, Archive, NotContextIndexed, and ReparsePoint
		///   (symbolic link) respectively.
		/// </para>
		///
		/// <para>
		///   To specify a time, use YYYY-MM-DD-HH:mm:ss or YYYY/MM/DD-HH:mm:ss as
		///   the format.  If you omit the HH:mm:ss portion, it is assumed to be
		///   00:00:00 (midnight).
		/// </para>
		///
		/// <para>
		///   The value for a size criterion is expressed in integer quantities of
		///   bytes, kilobytes (use k or kb after the number), megabytes (m or mb),
		///   or gigabytes (g or gb).
		/// </para>
		///
		/// <para>
		///   The value for a name is a pattern to match against the filename,
		///   potentially including wildcards.  The pattern follows CMD.exe glob
		///   rules: * implies one or more of any character, while ?  implies one
		///   character.  If the name pattern contains any slashes, it is matched to
		///   the entire filename, including the path; otherwise, it is matched
		///   against only the filename without the path.  This means a pattern of
		///   "*\*.*" matches all files one directory level deep, while a pattern of
		///   "*.*" matches all files in all directories.
		/// </para>
		///
		/// <para>
		///   To specify a name pattern that includes spaces, use single quotes
		///   around the pattern.  A pattern of "'* *.*'" will match all files that
		///   have spaces in the filename.  The full criteria string for that would
		///   be "name = '* *.*'" .
		/// </para>
		///
		/// <para>
		///   The value for a type criterion is either F (implying a file) or D
		///   (implying a directory).
		/// </para>
		///
		/// <para>
		///   Some examples:
		/// </para>
		///
		/// <list type="table">
		///   <listheader>
		///     <term>criteria</term>
		///     <description>Files retrieved</description>
		///   </listheader>
		///
		///   <item>
		///     <term>name != *.xls </term>
		///     <description>any file with an extension that is not .xls
		///     </description>
		///   </item>
		///
		///   <item>
		///     <term>name = *.mp3 </term>
		///     <description>any file with a .mp3 extension.
		///     </description>
		///   </item>
		///
		///   <item>
		///     <term>*.mp3</term>
		///     <description>(same as above) any file with a .mp3 extension.
		///     </description>
		///   </item>
		///
		///   <item>
		///     <term>attributes = A </term>
		///     <description>all files whose attributes include the Archive bit.
		///     </description>
		///   </item>
		///
		///   <item>
		///     <term>attributes != H </term>
		///     <description>all files whose attributes do not include the Hidden bit.
		///     </description>
		///   </item>
		///
		///   <item>
		///     <term>mtime > 2009-01-01</term>
		///     <description>all files with a last modified time after January 1st, 2009.
		///     </description>
		///   </item>
		///
		///   <item>
		///     <term>ctime > 2009/01/01-03:00:00</term>
		///     <description>all files with a created time after 3am (local time),
		///     on January 1st, 2009.
		///     </description>
		///   </item>
		///
		///   <item>
		///     <term>size > 2gb</term>
		///     <description>all files whose uncompressed size is greater than 2gb.
		///     </description>
		///   </item>
		///
		///   <item>
		///     <term>type = D</term>
		///     <description>all directories in the filesystem. </description>
		///   </item>
		///
		/// </list>
		///
		/// <para>
		///   You can combine criteria with the conjunctions AND, OR, and XOR. Using
		///   a string like "name = *.txt AND size &gt;= 100k" for the
		///   selectionCriteria retrieves entries whose names end in .txt, and whose
		///   uncompressed size is greater than or equal to 100 kilobytes.
		/// </para>
		///
		/// <para>
		///   For more complex combinations of criteria, you can use parenthesis to
		///   group clauses in the boolean logic.  Absent parenthesis, the
		///   precedence of the criterion atoms is determined by order of
		///   appearance.  Unlike the C# language, the AND conjunction does not take
		///   precendence over the logical OR.  This is important only in strings
		///   that contain 3 or more criterion atoms.  In other words, "name = *.txt
		///   and size &gt; 1000 or attributes = H" implies "((name = *.txt AND size
		///   &gt; 1000) OR attributes = H)" while "attributes = H OR name = *.txt
		///   and size &gt; 1000" evaluates to "((attributes = H OR name = *.txt)
		///   AND size &gt; 1000)".  When in doubt, use parenthesis.
		/// </para>
		///
		/// <para>
		///   Using time properties requires some extra care. If you want to
		///   retrieve all entries that were last updated on 2009 February 14,
		///   specify "mtime &gt;= 2009-02-14 AND mtime &lt; 2009-02-15".  Read this
		///   to say: all files updated after 12:00am on February 14th, until
		///   12:00am on February 15th.  You can use the same bracketing approach to
		///   specify any time period - a year, a month, a week, and so on.
		/// </para>
		///
		/// <para>
		///   The syntax allows one special case: if you provide a string with no
		///   spaces, it is treated as a pattern to match for the filename.
		///   Therefore a string like "*.xls" will be equivalent to specifying "name
		///   = *.xls".  This "shorthand" notation does not work with compound
		///   criteria.
		/// </para>
		///
		/// <para>
		///   There is no logic in this class that insures that the inclusion
		///   criteria are internally consistent.  For example, it's possible to
		///   specify criteria that says the file must have a size of less than 100
		///   bytes, as well as a size that is greater than 1000 bytes.  Obviously
		///   no file will ever satisfy such criteria, but this class does not check
		///   for or detect such inconsistencies.
		/// </para>
		///
		/// </remarks>
		///
		/// <exception cref="System.Exception">
		///   Thrown in the setter if the value has an invalid syntax.
		/// </exception>
		public String SelectionCriteria
		{
			get
			{
				if (_Criterion == null) return null;
				return _Criterion.ToString();
			}
			set
			{
				if (value == null) _Criterion = null;
				else if (value.Trim() == "") _Criterion = null;
				else
					_Criterion = _ParseCriterion(value);
			}
		}

		/// <summary>
		///  Indicates whether searches will traverse NTFS reparse points, like Junctions.
		/// </summary>
		public bool TraverseReparsePoints
		{
			get;
			set;
		}


		private enum ParseState
		{
			Start,
			OpenParen,
			CriterionDone,
			ConjunctionPending,
			Whitespace,
		}


		private static class RegexAssertions
		{
			public static readonly String PrecededByOddNumberOfSingleQuotes = "(?<=(?:[^']*'[^']*')*'[^']*)";
			public static readonly String FollowedByOddNumberOfSingleQuotesAndLineEnd = "(?=[^']*'(?:[^']*'[^']*')*[^']*$)";

			public static readonly String PrecededByEvenNumberOfSingleQuotes = "(?<=(?:[^']*'[^']*')*[^']*)";
			public static readonly String FollowedByEvenNumberOfSingleQuotesAndLineEnd = "(?=(?:[^']*'[^']*')*[^']*$)";
		}


		private static string NormalizeCriteriaExpression(string source)
		{
			// The goal here is to normalize the criterion expression. At output, in
			// the transformed criterion string, every significant syntactic element
			// - a property element, grouping paren for the boolean logic, operator
			// ( = < > != ), conjunction, or property value - will be separated from
			// its neighbors by at least one space. Thus,
			//
			// before                         after
			// -------------------------------------------------------------------
			// name=*.txt                     name = *.txt
			// (size>100)AND(name=*.txt)      ( size > 100 ) AND ( name = *.txt )
			//
			// This is relatively straightforward using regular expression
			// replacement. This method applies a distinct regex pattern and
			// corresponding replacement string for each one of a number of cases:
			// an open paren followed by a word; a word followed by a close-paren; a
			// pair of open parens; a close paren followed by a word (which should
			// then be followed by an open paren). And so on. These patterns and
			// replacements are all stored in prPairs. By applying each of these
			// regex replacements in turn, we get the transformed string. Easy.
			//
			// The resulting "normalized" criterion string, is then used as the
			// subject that gets parsed, by splitting the string into tokens that
			// are separated by spaces.  Here, there's a twist. The spaces within
			// single-quote delimiters do not delimit distinct tokens.  So, this
			// normalization method temporarily replaces those spaces with
			// ASCII 6 (0x06), a control character which is not a legal
			// character in a filename. The parsing logic that happens later will
			// revert that change, restoring the original value of the filename
			// specification.
			//
			// To illustrate, for a "before" string of [(size>100)AND(name='Name
			// (with Parens).txt')] , the "after" string is [( size > 100 ) AND
			// ( name = 'Name\u0006(with\u0006Parens).txt' )].
			//

			string[][] prPairs =
                {
                    // A. opening double parens - insert a space between them
                    new string[] { @"([^']*)\(\(([^']+)", "$1( ($2" },

                    // B. closing double parens - insert a space between
                    new string[] { @"(.)\)\)", "$1) )" },

                    // C. single open paren with a following word - insert a space between
                    new string[] { @"\((\S)", "( $1" },

                    // D. single close paren with a preceding word - insert a space between the two
                    new string[] { @"(\S)\)", "$1 )" },

                    // E. close paren at line start?, insert a space before the close paren
                    // this seems like a degenerate case.  I don't recall why it's here.
                    new string[] { @"^\)", " )" },

                    // F. a word (likely a conjunction) followed by an open paren - insert a space between
                    new string[] { @"(\S)\(", "$1 (" },

                    // G. single close paren followed by word - insert a paren after close paren
                    new string[] { @"\)(\S)", ") $1" },

                    // H. insert space between = and a following single quote
                    //new string[] { @"(=|!=)('[^']*')", "$1 $2" },
                    new string[] { @"(=)('[^']*')", "$1 $2" },

                    // I. insert space between property names and the following operator
                    //new string[] { @"([^ ])([><(?:!=)=])", "$1 $2" },
                    new string[] { @"([^ !><])(>|<|!=|=)", "$1 $2" },

                    // J. insert spaces between operators and the following values
                    //new string[] { @"([><(?:!=)=])([^ ])", "$1 $2" },
                    new string[] { @"(>|<|!=|=)([^ =])", "$1 $2" },

                    // K. replace fwd slash with backslash
                    new string[] { @"/", "\\" },
                };

			string interim = source;

			for (int i = 0; i < prPairs.Length; i++)
			{
				//char caseIdx = (char)('A' + i);
				string pattern = RegexAssertions.PrecededByEvenNumberOfSingleQuotes +
					prPairs[i][0] +
					RegexAssertions.FollowedByEvenNumberOfSingleQuotesAndLineEnd;

				interim = Regex.Replace(interim, pattern, prPairs[i][1]);
			}

			// match a fwd slash, followed by an odd number of single quotes.
			// This matches fwd slashes only inside a pair of single quote delimiters,
			// eg, a filename.  This must be done as well as the case above, to handle
			// filenames specified inside quotes as well as filenames without quotes.
			var regexPattern = @"/" +
								RegexAssertions.FollowedByOddNumberOfSingleQuotesAndLineEnd;
			// replace with backslash
			interim = Regex.Replace(interim, regexPattern, "\\");

			// match a space, followed by an odd number of single quotes.
			// This matches spaces only inside a pair of single quote delimiters.
			regexPattern = " " +
				RegexAssertions.FollowedByOddNumberOfSingleQuotesAndLineEnd;

			// Replace all spaces that appear inside single quotes, with
			// ascii 6.  This allows a split on spaces to get tokens in
			// the expression. The split will not split any filename or
			// wildcard that appears within single quotes. After tokenizing, we
			// need to replace ascii 6 with ascii 32 to revert the
			// spaces within quotes.
			return Regex.Replace(interim, regexPattern, "\u0006");
		}


		private static SelectionCriterion _ParseCriterion(String s)
		{
			if (s == null) return null;

			// inject spaces after open paren and before close paren, etc
			s = NormalizeCriteriaExpression(s);

			// no spaces in the criteria is shorthand for filename glob
			if (s.IndexOf(" ") == -1)
				s = "name = " + s;

			// split the expression into tokens
			string[] tokens = s.Trim().Split(' ', '\t');

			if (tokens.Length < 3) throw new ArgumentException(s);

			SelectionCriterion current = null;

			LogicalConjunction pendingConjunction = LogicalConjunction.NONE;

			ParseState state;
			var stateStack = new System.Collections.Generic.Stack<ParseState>();
			var critStack = new System.Collections.Generic.Stack<SelectionCriterion>();
			stateStack.Push(ParseState.Start);

			for (int i = 0; i < tokens.Length; i++)
			{
				string tok1 = tokens[i].ToLower();
				switch (tok1)
				{
					case "and":
					case "xor":
					case "or":
						state = stateStack.Peek();
						if (state != ParseState.CriterionDone)
							throw new ArgumentException(String.Join(" ", tokens, i, tokens.Length - i));

						if (tokens.Length <= i + 3)
							throw new ArgumentException(String.Join(" ", tokens, i, tokens.Length - i));

						pendingConjunction = (LogicalConjunction)Enum.Parse(typeof(LogicalConjunction), tokens[i].ToUpper(), true);
						current = new CompoundCriterion { Left = current, Right = null, Conjunction = pendingConjunction };
						stateStack.Push(state);
						stateStack.Push(ParseState.ConjunctionPending);
						critStack.Push(current);
						break;

					case "(":
						state = stateStack.Peek();
						if (state != ParseState.Start && state != ParseState.ConjunctionPending && state != ParseState.OpenParen)
							throw new ArgumentException(String.Join(" ", tokens, i, tokens.Length - i));

						if (tokens.Length <= i + 4)
							throw new ArgumentException(String.Join(" ", tokens, i, tokens.Length - i));

						stateStack.Push(ParseState.OpenParen);
						break;

					case ")":
						state = stateStack.Pop();
						if (stateStack.Peek() != ParseState.OpenParen)
							throw new ArgumentException(String.Join(" ", tokens, i, tokens.Length - i));

						stateStack.Pop();
						stateStack.Push(ParseState.CriterionDone);
						break;

					case "atime":
					case "ctime":
					case "mtime":
						if (tokens.Length <= i + 2)
							throw new ArgumentException(String.Join(" ", tokens, i, tokens.Length - i));

						DateTime t;
						try
						{
							t = DateTime.ParseExact(tokens[i + 2], "yyyy-MM-dd-HH:mm:ss", null);
						}
						catch (FormatException)
						{
							try
							{
								t = DateTime.ParseExact(tokens[i + 2], "yyyy/MM/dd-HH:mm:ss", null);
							}
							catch (FormatException)
							{
								try
								{
									t = DateTime.ParseExact(tokens[i + 2], "yyyy/MM/dd", null);
								}
								catch (FormatException)
								{
									try
									{
										t = DateTime.ParseExact(tokens[i + 2], "MM/dd/yyyy", null);
									}
									catch (FormatException)
									{
										t = DateTime.ParseExact(tokens[i + 2], "yyyy-MM-dd", null);
									}
								}
							}
						}
						t = DateTime.SpecifyKind(t, DateTimeKind.Local).ToUniversalTime();
						current = new TimeCriterion
						{
							Which = (WhichTime)Enum.Parse(typeof(WhichTime), tokens[i], true),
							Operator = (ComparisonOperator)EnumUtil.Parse(typeof(ComparisonOperator), tokens[i + 1]),
							Time = t
						};
						i += 2;
						stateStack.Push(ParseState.CriterionDone);
						break;


					case "length":
					case "size":
						if (tokens.Length <= i + 2)
							throw new ArgumentException(String.Join(" ", tokens, i, tokens.Length - i));

						Int64 sz = 0;
						string v = tokens[i + 2];
						if (v.ToUpper().EndsWith("K"))
							sz = Int64.Parse(v.Substring(0, v.Length - 1)) * 1024;
						else if (v.ToUpper().EndsWith("KB"))
							sz = Int64.Parse(v.Substring(0, v.Length - 2)) * 1024;
						else if (v.ToUpper().EndsWith("M"))
							sz = Int64.Parse(v.Substring(0, v.Length - 1)) * 1024 * 1024;
						else if (v.ToUpper().EndsWith("MB"))
							sz = Int64.Parse(v.Substring(0, v.Length - 2)) * 1024 * 1024;
						else if (v.ToUpper().EndsWith("G"))
							sz = Int64.Parse(v.Substring(0, v.Length - 1)) * 1024 * 1024 * 1024;
						else if (v.ToUpper().EndsWith("GB"))
							sz = Int64.Parse(v.Substring(0, v.Length - 2)) * 1024 * 1024 * 1024;
						else sz = Int64.Parse(tokens[i + 2]);

						current = new SizeCriterion
						{
							Size = sz,
							Operator = (ComparisonOperator)EnumUtil.Parse(typeof(ComparisonOperator), tokens[i + 1])
						};
						i += 2;
						stateStack.Push(ParseState.CriterionDone);
						break;

					case "filename":
					case "name":
						{
							if (tokens.Length <= i + 2)
								throw new ArgumentException(String.Join(" ", tokens, i, tokens.Length - i));

							ComparisonOperator c =
								(ComparisonOperator)EnumUtil.Parse(typeof(ComparisonOperator), tokens[i + 1]);

							if (c != ComparisonOperator.NotEqualTo && c != ComparisonOperator.EqualTo)
								throw new ArgumentException(String.Join(" ", tokens, i, tokens.Length - i));

							string m = tokens[i + 2];

							// handle single-quoted filespecs (used to include
							// spaces in filename patterns)
							if (m.StartsWith("'") && m.EndsWith("'"))
							{
								// trim off leading and trailing single quotes and
								// revert the control characters to spaces.
								m = m.Substring(1, m.Length - 2)
									.Replace("\u0006", " ");
							}

							// if (m.StartsWith("'"))
							//     m = m.Replace("\u0006", " ");

							current = new NameCriterion
							{
								MatchingFileSpec = m,
								Operator = c
							};
							i += 2;
							stateStack.Push(ParseState.CriterionDone);
						}
						break;

#if !SILVERLIGHT
					case "attrs":
					case "attributes":
#endif
					case "type":
						{
							if (tokens.Length <= i + 2)
								throw new ArgumentException(String.Join(" ", tokens, i, tokens.Length - i));

							ComparisonOperator c =
								(ComparisonOperator)EnumUtil.Parse(typeof(ComparisonOperator), tokens[i + 1]);

							if (c != ComparisonOperator.NotEqualTo && c != ComparisonOperator.EqualTo)
								throw new ArgumentException(String.Join(" ", tokens, i, tokens.Length - i));

#if SILVERLIGHT
                            current = (SelectionCriterion) new TypeCriterion
                                    {
                                        AttributeString = tokens[i + 2],
                                        Operator = c
                                    };
#else
							current = (tok1 == "type")
								? (SelectionCriterion)new TypeCriterion
									{
										AttributeString = tokens[i + 2],
										Operator = c
									}
								: (SelectionCriterion)new AttributesCriterion
									{
										AttributeString = tokens[i + 2],
										Operator = c
									};
#endif
							i += 2;
							stateStack.Push(ParseState.CriterionDone);
						}
						break;

					case "":
						// NOP
						stateStack.Push(ParseState.Whitespace);
						break;

					default:
						throw new ArgumentException("'" + tokens[i] + "'");
				}

				state = stateStack.Peek();
				if (state == ParseState.CriterionDone)
				{
					stateStack.Pop();
					if (stateStack.Peek() == ParseState.ConjunctionPending)
					{
						while (stateStack.Peek() == ParseState.ConjunctionPending)
						{
							var cc = critStack.Pop() as CompoundCriterion;
							cc.Right = current;
							current = cc; // mark the parent as current (walk up the tree)
							stateStack.Pop();   // the conjunction is no longer pending

							state = stateStack.Pop();
							if (state != ParseState.CriterionDone)
								throw new ArgumentException("??");
						}
					}
					else stateStack.Push(ParseState.CriterionDone);  // not sure?
				}

				if (state == ParseState.Whitespace)
					stateStack.Pop();
			}

			return current;
		}


		/// <summary>
		/// Returns a string representation of the FileSelector object.
		/// </summary>
		/// <returns>The string representation of the boolean logic statement of the file
		/// selection criteria for this instance. </returns>
		public override String ToString()
		{
			return "FileSelector(" + _Criterion.ToString() + ")";
		}


		private bool Evaluate(string filename)
		{
			// dinoch - Thu, 11 Feb 2010  18:34
			SelectorTrace("Evaluate({0})", filename);
			bool result = _Criterion.Evaluate(filename);
			return result;
		}

		[System.Diagnostics.Conditional("SelectorTrace")]
		private void SelectorTrace(string format, params object[] args)
		{
			if (_Criterion != null && _Criterion.Verbose)
				System.Console.WriteLine(format, args);
		}

		/// <summary>
		///   Returns the names of the files in the specified directory
		///   that fit the selection criteria specified in the FileSelector.
		/// </summary>
		///
		/// <remarks>
		///   This is equivalent to calling <see cref="SelectFiles(String, bool)"/>
		///   with recurseDirectories = false.
		/// </remarks>
		///
		/// <param name="directory">
		///   The name of the directory over which to apply the FileSelector
		///   criteria.
		/// </param>
		///
		/// <returns>
		///   A collection of strings containing fully-qualified pathnames of files
		///   that match the criteria specified in the FileSelector instance.
		/// </returns>
		public System.Collections.Generic.ICollection<String> SelectFiles(String directory)
		{
			return SelectFiles(directory, false);
		}


		/// <summary>
		///   Returns the names of the files in the specified directory that fit the
		///   selection criteria specified in the FileSelector, optionally recursing
		///   through subdirectories.
		/// </summary>
		///
		/// <remarks>
		///   This method applies the file selection criteria contained in the
		///   FileSelector to the files contained in the given directory, and
		///   returns the names of files that conform to the criteria.
		/// </remarks>
		///
		/// <param name="directory">
		///   The name of the directory over which to apply the FileSelector
		///   criteria.
		/// </param>
		///
		/// <param name="recurseDirectories">
		///   Whether to recurse through subdirectories when applying the file
		///   selection criteria.
		/// </param>
		///
		/// <returns>
		///   A collection of strings containing fully-qualified pathnames of files
		///   that match the criteria specified in the FileSelector instance.
		/// </returns>
		public System.Collections.ObjectModel.ReadOnlyCollection<String>
			SelectFiles(String directory,
						bool recurseDirectories)
		{
			if (_Criterion == null)
				throw new ArgumentException("SelectionCriteria has not been set");

			var list = new List<String>();
			try
			{
				if (Directory.Exists(directory))
				{
					String[] filenames = Directory.GetFiles(directory);

					// add the files:
					foreach (String filename in filenames)
					{
						if (Evaluate(filename))
							list.Add(filename);
					}

					if (recurseDirectories)
					{
						// add the subdirectories:
						String[] dirnames = Directory.GetDirectories(directory);
						foreach (String dir in dirnames)
						{
							if (this.TraverseReparsePoints
#if !SILVERLIGHT
 || ((File.GetAttributes(dir) & FileAttributes.ReparsePoint) == 0)
#endif
)
							{
								// workitem 10191
								if (Evaluate(dir)) list.Add(dir);
								list.AddRange(this.SelectFiles(dir, recurseDirectories));
							}
						}
					}
				}
			}
			// can get System.UnauthorizedAccessException here
			catch (System.UnauthorizedAccessException)
			{
			}
			catch (System.IO.IOException)
			{
			}

			return list.AsReadOnly();
		}
	}



	/// <summary>
	/// Summary description for EnumUtil.
	/// </summary>
	internal sealed class EnumUtil
	{
		private EnumUtil() { }
		/// <summary>
		///   Returns the value of the DescriptionAttribute if the specified Enum
		///   value has one.  If not, returns the ToString() representation of the
		///   Enum value.
		/// </summary>
		/// <param name="value">The Enum to get the description for</param>
		/// <returns></returns>
		internal static string GetDescription(System.Enum value)
		{
			FieldInfo fi = value.GetType().GetField(value.ToString());
			var attributes = (DescriptionAttribute[])fi.GetCustomAttributes(typeof(DescriptionAttribute), false);
			if (attributes.Length > 0)
				return attributes[0].Description;
			else
				return value.ToString();
		}

		/// <summary>
		///   Converts the string representation of the name or numeric value of one
		///   or more enumerated constants to an equivalent enumerated object.
		///   Note: use the DescriptionAttribute on enum values to enable this.
		/// </summary>
		/// <param name="enumType">The System.Type of the enumeration.</param>
		/// <param name="stringRepresentation">
		///   A string containing the name or value to convert.
		/// </param>
		/// <returns></returns>
		internal static object Parse(Type enumType, string stringRepresentation)
		{
			return Parse(enumType, stringRepresentation, false);
		}


#if SILVERLIGHT
       public static System.Enum[] GetEnumValues(Type type)
        {
            if (!type.IsEnum)
                throw new ArgumentException("not an enum");

            return (
              from field in type.GetFields(BindingFlags.Public | BindingFlags.Static)
              where field.IsLiteral
              select (System.Enum)field.GetValue(null)
            ).ToArray();
        }

        public static string[] GetEnumStrings<T>()
        {
            var type = typeof(T);
            if (!type.IsEnum)
                throw new ArgumentException("not an enum");

            return (
              from field in type.GetFields(BindingFlags.Public | BindingFlags.Static)
              where field.IsLiteral
              select field.Name
            ).ToArray();
        }
#endif

		/// <summary>
		///   Converts the string representation of the name or numeric value of one
		///   or more enumerated constants to an equivalent enumerated object.  A
		///   parameter specified whether the operation is case-sensitive.  Note:
		///   use the DescriptionAttribute on enum values to enable this.
		/// </summary>
		/// <param name="enumType">The System.Type of the enumeration.</param>
		/// <param name="stringRepresentation">
		///   A string containing the name or value to convert.
		/// </param>
		/// <param name="ignoreCase">
		///   Whether the operation is case-sensitive or not.</param>
		/// <returns></returns>
		internal static object Parse(Type enumType, string stringRepresentation, bool ignoreCase)
		{
			if (ignoreCase)
				stringRepresentation = stringRepresentation.ToLower();

#if SILVERLIGHT
            foreach (System.Enum enumVal in GetEnumValues(enumType))
#else
			foreach (System.Enum enumVal in System.Enum.GetValues(enumType))
#endif
			{
				string description = GetDescription(enumVal);
				if (ignoreCase)
					description = description.ToLower();
				if (description == stringRepresentation)
					return enumVal;
			}

			return System.Enum.Parse(enumType, stringRepresentation, ignoreCase);
		}
	}


#if DEMO
    public class DemonstrateFileSelector
    {
        private string _directory;
        private bool _recurse;
        private bool _traverse;
        private bool _verbose;
        private string _selectionCriteria;
        private FileSelector f;

        public DemonstrateFileSelector()
        {
            this._directory = ".";
            this._recurse = true;
        }

        public DemonstrateFileSelector(string[] args) : this()
        {
            for (int i = 0; i < args.Length; i++)
            {
                switch(args[i])
                {
                case"-?":
                    Usage();
                    Environment.Exit(0);
                    break;
                case "-d":
                    i++;
                    if (args.Length <= i)
                        throw new ArgumentException("-directory");
                    this._directory = args[i];
                    break;
                case "-norecurse":
                    this._recurse = false;
                    break;

                case "-j-":
                    this._traverse = false;
                    break;

                case "-j+":
                    this._traverse = true;
                    break;

                case "-v":
                    this._verbose = true;
                    break;

                default:
                    if (this._selectionCriteria != null)
                        throw new ArgumentException(args[i]);
                    this._selectionCriteria = args[i];
                    break;
                }

                if (this._selectionCriteria != null)
                    this.f = new FileSelector(this._selectionCriteria);
            }
        }


        public static void Main(string[] args)
        {
            try
            {
                Console.WriteLine();
                new DemonstrateFileSelector(args).Run();
            }
            catch (Exception exc1)
            {
                Console.WriteLine("Exception: {0}", exc1.ToString());
                Usage();
            }
        }


        public void Run()
        {
            if (this.f == null)
                this.f = new FileSelector("name = *.jpg AND (size > 1000 OR atime < 2009-02-14-01:00:00)");

            this.f.TraverseReparsePoints = _traverse;
            this.f.Verbose = this._verbose;
            Console.WriteLine();
            Console.WriteLine(new String(':', 88));
            Console.WriteLine("Selecting files:\n" + this.f.ToString());
            var files = this.f.SelectFiles(this._directory, this._recurse);
            if (files.Count == 0)
            {
                Console.WriteLine("no files.");
            }
            else
            {
                Console.WriteLine("files: {0}", files.Count);
                foreach (string file in files)
                {
                    Console.WriteLine("  " + file);
                }
            }
        }

        public static void Usage()
        {
            Console.WriteLine("FileSelector: select files based on selection criteria.\n");
            Console.WriteLine("Usage:\n  FileSelector <selectionCriteria>  [options]\n" +
                              "\n" +
                              "  -d <dir>   directory to select from (Default .)\n" +
                              " -norecurse  don't recurse into subdirs\n" +
                              " -j-         don't traverse junctions\n" +
                              " -v          verbose output\n");
        }
    }

#endif



}
